中文: 什么是闭包(Closure)?请用你自己的话解释一下,并举一个简单例子。
English: What is a closure in JavaScript? Explain it in your own words and give a simple example.
中文: 闭包是指:函数可以访问并记住它定义时所在的作用域,即使这个函数在其作用域之外执行。
English: A closure is a function that remembers and can access the variables from its lexical scope even when it is executed outside that scope.
x
function a() { let count = 0; return function b(){ count++; console.log(count); }}const increment = a();increment();increment();A closure is created when a function (function b) "remembers" and continues to access the variables from its outer scope (function a), even after the outer function has finished executing.
The count variable is private. It cannot be accessed or modified from the outside world; only the increment function can change it.
Closure allows a function to have "memory" without using global variables.
中文: 当一个函数(函数 b)能够“记住”并持续访问其外部作用域(函数 a)中的变量时,即使外部函数已经执行完毕,这就形成了一个闭包。
中文: 为什么 JavaScript 会产生闭包?是语言“设计出来的”,还是“自然形成的”?
English: Why do closures exist in JavaScript? Are they intentionally designed, or do they occur naturally?
中文: 闭包不是一个“额外设计的特性”,而是 JavaScript 词法作用域(Lexical Scope)的自然结果。
闭包的本质不是“特殊机制”,而是 作用域链 + 垃圾回收机制共同作用的结果。
English: Closures are not an extra feature intentionally added — they are a natural result of JavaScript’s lexical scoping.
A closure is the continuation of lexical scope beyond the function’s execution lifecycle.
什么是词法作用域(关键铺垫)
中文: JavaScript 在函数定义的时候就确定了作用域,而不是在执行的时候。
也就是说: 函数能访问哪些变量,在写代码的时候就已经决定了。
English: In JavaScript, scope is determined at function definition time, not at runtime.
That means: What variables a function can access is fixed when the code is written.
闭包是怎么“自然出现”的
看这个例子:
xxxxxxxxxxfunction outer() { let x = 10; function inner() { console.log(x); } return inner;}const fn = outer();fn();中文解释:
inner 在定义时,就“记住”了它的作用域(包含 x)outer 执行完了inner 仍然要访问 xx 留在内存中👉 于是闭包就“自然产生了”
English Explanation:
inner is defined, it captures its surrounding scope (including x)outer finishes executioninner still needs access to xx in memory👉 This naturally results in a closure
中文: 下面代码会输出什么?为什么?
xxxxxxxxxxfor (var i = 0; i < 3; i++) { setTimeout(() => { console.log(i); }, 0);}English: What will this code output, and why?
我的答案:2,2,2。因为我模糊的记得,setTimeout里面的即使延时时间是0,也会放到宏任务里面去,让for循环执行完成之后才会接着执行。拿到的i都是同一个值,所以我给出的答案是2,2,2。
但是我没有想到,我居然还没有理解for循环的执行流程。var i = 0仅仅执行一次。然后开始执行迭代,迭代的时候先检查条件,再执行循环体,再执行自增。
第一步:初始化(仅一次)
- 执行
var i = 0;。- 此时,全局作用域里多了一个变量
i,值是 0。第二步:第一次迭代
检查条件:
i < 3(0 < 3),结果为true。执行循环体:遇到
setTimeout。
- 注意:它只是把回调函数往“待办任务”里一扔,说:“等会儿再打印
i”。它并没有去读i的具体值。执行自增:
i++。此时i变成了 1。第三步:第二次迭代
- 检查条件:
i < 3(1 < 3),结果为true。- 执行循环体:又一个
setTimeout回调被丢进待办任务。- 执行自增:
i++。此时i变成了 2。第四步:第三次迭代
- 检查条件:
i < 3(2 < 3),结果为true。- 执行循环体:第三个
setTimeout回调被丢进待办任务。- 执行自增:
i++。此时i变成了 3。第五步:循环终点
- 检查条件:
i < 3(3 < 3),结果为false。- 跳出循环:循环结束,
i停留在 3。
分析:
i = 0:for 循环开始,执行 setTimeout。
setTimeout 的回调函数并没有执行,它被丢进了宏任务队列等待。i = 1:重复,又一个回调被丢进队列。
i = 2:重复,第三个回调被丢进队列。
i = 3:循环结束条件达成。此时,因为你使用的是 var,i 并没有被销毁,而是作为全局变量停留在了 3。
当同步代码(循环)全部跑完后,主线程空闲了,事件循环(Event Loop)开始把任务队列里的回调拿出来执行:
console.log(i)。此时它去寻找 i,发现全局作用域下的 i 已经是 3 了。console.log(i)。发现 i 还是 3。console.log(i)。发现 i 还是 3。
核心原因
var 定义的变量没有“块级作用域”。这意味着整个循环过程中,所有回调函数引用的都是同一个 i。setTimeout 的代码永远在循环结束后才运行。
中文标准答案:
因为 var 的作用域是函数级的,循环中只有一个 i 变量。
setTimeout 的回调函数都引用同一个 i。
当异步执行时,循环已经结束,此时 i === 3,所以输出 3 次 3。
闭包保存的是“变量的引用”,不是“变量的快照”。
English Standard Answer:
Because var is function-scoped, there is only one shared i variable.
All setTimeout callbacks reference the same i.
By the time the callbacks execute, the loop has finished and i === 3, so it prints 3 three times.
A closure captures a reference to a variable, not a snapshot of its value.
为什么使用let来定义i,结果不同呢?到底哪里不同呢?
先说明,还是存在闭包,只不过由于let会产生块级作用域,所以回调函数闭包捕获的是3个完全不同的变量。
使用
let之后,输出会变成0, 1, 2。这背后的核心差异在于 “作用域的创建次数” 和 “变量的绑定方式”。1.
var是“单间”,let是“公寓”
- 使用
var时:整个循环只创建了 一个 变量i。这就好比一个单间,所有人(所有的setTimeout)都盯着这一个房间看。当循环结束,房间里的数字变成了 3,所有回调看过去都是 3。- 使用
let时:JavaScript 引擎在每一轮循环迭代时,都会创建一个 全新的、独立的块级作用域。这就好比一栋公寓,每一层(每一轮循环)都有一个属于自己的变量i。2. 执行过程的微观拆解
当执行
for (let i = 0; i < 3; i++)时,逻辑是这样的:
第一轮 (i=0):
- 创建一个作用域块(我们叫它
Block_0),在这个块里声明let i = 0。setTimeout的回调被丢进队列,但它捕获的是Block_0里的这个i。第二轮 (i=1):
- JS 引擎会帮你在后台把上一轮的值传给这一轮。
- 创建一个新的作用域块
Block_1,声明let i = 1。setTimeout的回调捕获的是Block_1里的这个i。第三轮 (i=2):
- 创建
Block_2,声明let i = 2。- 回调捕获的是
Block_2里的i。3. 本质区别:闭包捕获了不同的变量
var版:3 个回调函数闭包捕获的是 同一个 变量引用(全局的那个i)。let版:3 个回调函数闭包捕获的是 3 个完全不同 的变量。尽管它们都叫i,但它们存在于不同的内存空间里。形象比喻:
var:你给 3 个小朋友(setTimeout)每人发了一张写着“去看黑板”的纸条。等你写完(循环结束),黑板上是 3,小朋友们去读,看到的都是 3。let:你当场给 3 个小朋友每人发了一张照片。第一张拍的是 0,第二张拍的是 1,第三张拍的是 2。无论你之后在黑板上写什么,小朋友手里照片里的数字是不会变的。总结
let的魔法在于:它在每次迭代时都重新声明了一个变量。这在底层其实是 JS 编译器自动帮你做了一层类似“函数传参”的包装。你现在明白为什么在实现
MyPromise内部逻辑时,我们更倾向于用let和const这种块级作用域来防止数据污染了吗?要不要试着解释一下,如果我们在Promise.myAll里用var来计数会发生什么?
中文: 闭包会导致内存泄漏吗?如果会,是为什么?如果不会,是在什么情况下会发生?
English: Can closures cause memory leaks in JavaScript? If yes, why? If not, under what conditions can it happen?
答案:
中文: 闭包本身不会导致内存泄漏,但使用不当的闭包可能导致内存泄漏。
English: Closures themselves do not cause memory leaks, but improper use of closures can lead to memory leaks.
中文: 因为闭包会让函数持续引用外部变量,导致这些变量无法被垃圾回收(GC)释放。
English: Because closures keep references to outer variables, those variables cannot be garbage collected as long as they are still referenced.
中文:
内存泄漏的本质是:不再需要的数据仍然被引用
如果闭包不再被引用,它和它引用的变量都会被回收 → 不会内存泄漏。
English:
The essence of a memory leak is: Data that is no longer needed is still being referenced If the closure is no longer referenced, both the closure and its captured variables will be garbage collected → no memory leak.
如何避免内存泄漏:
中文:
English:
中文: 闭包和作用域链(scope chain)有什么关系?它们是同一个东西吗?
English: What is the relationship between closures and the scope chain? Are they the same thing?
答案:
中文: 闭包和作用域链不是同一个东西,但闭包是基于作用域链产生的结果。
English: Closures and the scope chain are not the same thing, but closures are a result of the scope chain.
中文: 作用域链是 JavaScript 查找变量的一种机制:
English: The scope chain is the mechanism JavaScript uses to resolve variables:
还可以结合实际例子来解释:
xxxxxxxxxxfunction outer() { let x = 10; return function inner() { console.log(x); }}中文解释:
inner 在定义时就绑定了作用域链(包含 outer)outer 执行完应该被销毁inner 还要通过作用域链访问 x👉 这就形成了闭包
English Explanation:
inner is defined, it gets a scope chain including outerouter would be destroyed after executioninner still needs access to x via the scope chain👉 This results in a closure
本质区别:
| 概念 | 本质 |
|---|---|
| 作用域链 | 查找变量的机制 |
| 闭包 | 作用域被“保留下来”的现象 |
中文总结:
English Summary:
一句话答案:
中文: 闭包是由于函数在执行后仍然通过作用域链访问变量,从而导致作用域无法被释放的现象。
English: A closure occurs when a function continues to access variables via the scope chain after its outer function has finished execution.
中文: 闭包在实际开发中有什么应用?请举 2-3 个真实场景(不要只说“缓存变量”这种空话)。
English: What are real-world use cases of closures? Give 2–3 practical examples (avoid vague answers like “caching variables”).
注意针对react来说明即可,场景有很多,但是很难理解,如果不理解,就问AI。
1、自定义 Hook (封装逻辑)
闭包允许自定义 Hook 维持私有状态并暴露方法给组件使用。
xxxxxxxxxxfunction useCounter() { const [count, setCount] = useState(0); // 这些方法都是闭包,它们共享同一个 count 状态 const increment = () => setCount(c => c + 1); const decrement = () => setCount(c => c - 1); return { count, increment, decrement };}2、函数式更新(解决闭包陈旧问题)
为了避开闭包捕获旧状态的问题,React 提供了一种不依赖闭包变量的更新方式:
xxxxxxxxxxfunction Increment() { const [count, setCount] = useState(0); const handleClick = () => { // 这里的 prev => prev + 1 不再依赖闭包里的 count 变量。与此相对的是 setCount(count + 1),这里的count就依赖闭包里面的 count 变量。 // 而是直接向 React 索要最新的当前值 setCount(prev => prev + 1); }; return <button onClick={handleClick}>Add</button>;}中文: 函数式更新可以避免闭包捕获旧值的问题。
English: Functional updates avoid stale closures by always using the latest state.
3、useEffect 捕获状态
这是闭包最直观的应用。当你使用 useEffect 时,它内部定义的函数会“捕获”当前那一次渲染中的变量。
xxxxxxxxxxfunction Counter() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { // 这里的 count 捕获的是 useEffect 定义时的那一版 count console.log("当前计数是:", count); }, 1000); return () => clearInterval(timer); }, []); // 注意:如果依赖项为空,这个闭包永远只记得 count 初次渲染时的 0 return <h1>{count}</h1>;}count 的引用。count,闭包会产生“陈旧数据(Stale Closures)”,即便 UI 变了,定时器打印的永远是 0。中文: 因为 useEffect 的回调函数形成了闭包,它捕获的是初始 render 时的 count,而不是最新值。
English:
The callback inside useEffect forms a closure that captures the initial value of count, not the updated one.
“闭包是指有权访问另一个函数作用域中变量的函数。在 React 中,它的应用非常广泛,比如
useEffect或回调函数都会捕获当前的state和props。它的核心价值在于持久化局部变量的状态,但也容易导致‘陈旧闭包’问题(比如异步操作拿不到最新的 state),这时我们需要通过useRef或setState的函数式更新来解决。”
在 React 中,闭包主要用于状态的捕获和隔离。每次组件 render 时,都会生成新的函数,这些函数通过闭包绑定当前的 state。这既是 React 能够实现函数式组件的基础,也是 stale state 等问题产生的原因。
那react是怎么处理的呢?
中文: React 不解决 stale closure,是因为闭包是 JavaScript 的语言特性,同时 React 需要保持 render 的纯函数特性。如果自动更新闭包,会导致性能问题和不可预测行为。因此 React 选择通过依赖数组和函数式更新等机制,把控制权交给开发者。
English: React does not fix stale closures because closures are a JavaScript language feature, and React must preserve the purity of renders. Automatically updating closures would cause performance issues and unpredictable behavior. Instead, React provides tools like dependency arrays and functional updates to give developers control.
中文:
请你解释一下 JavaScript 中的 this 是什么?它是如何确定的?
English:
What is this in JavaScript, and how is it determined?
中文答案:
this 可以理解为函数运行时的上下文对象,它指向谁不是在定义的时候决定的,而是在调用的时候决定的。 一般来说要看函数是怎么被调用的,比如普通函数调用、对象调用、call/apply/bind,或者 new 调用,不同方式 this 会不一样。
English 口语版(自然表达)
thisrefers to the execution context of a function. Its value is not determined when the function is defined, but when it is called. It depends on how the function is invoked, such as normal calls, method calls, or using call/apply/bind, or using new.
如果面试官有追问,那么就按照下面4种情况来详细说明:
可以简单分四种情况: 第一种是默认绑定,也就是普通函数直接调用,在非严格模式下 this 指向 window,严格模式下是 undefined。 第二种是隐式绑定,也就是通过对象调用,比如 obj.fn(),this 指向这个对象本身。 第三种是显式绑定,就是通过 call、apply 或 bind 去指定 this,这种情况下 this 就指向你传进去的那个对象。 第四种是 new 绑定,如果用 new 去调用函数,this 会指向新创建的实例对象。
There are mainly four cases:
For a normal function call, this is window in non-strict mode or undefined in strict mode.
For method calls, this refers to the object that calls the function.
With call, apply, or bind, this is explicitly set to the provided object.
And with new, this refers to the newly created instance.
xxxxxxxxxxconst obj = { name: "Tom", fn: function () { console.log(this.name); }};const fn = obj.fn;fn();输出什么?为什么?
分析:
这一题的本质(你必须理解):this 丢失(this loss)
this指向,由调用时决定,所以这里返回undefined。
中文标准口语版:
这里会输出 undefined。因为虽然这个函数最初是定义在 obj 里的,但在赋值给 fn 之后,是以普通函数的方式调用的。 根据默认绑定规则,this 在非严格模式下会指向 window,而 window 上没有 name 这个属性,所以是 undefined。
这是一个典型的 this 丢失问题。
English 口语版:
It outputs undefined. Even though the function is defined inside the object, once it's assigned to
fn, it is called as a normal function. According to the default binding rule,thisrefers towindowin non-strict mode, and sincewindowdoesn't have anameproperty, it prints undefined.This is a classic case of
thisbeing lost.
中文: 那你怎么解决 this 丢失问题?
English:
How would you fix the this binding issue in this case?
中文口语版
this 丢失一般有几种解决方式。 第一种是使用 bind 显式绑定 this;bind 会返回一个新的函数,并且把 this 永久绑定住。 第二种是用箭头函数,因为箭头函数没有自己的 this,会继承外层的 this; 还有一种是在调用的时候用 call 或 apply 指定 this。call 和 apply 是在调用时临时指定 this。
解决 this 丢失的本质是:确保函数调用时的 this 是我们期望的对象。
English 口语版
There are a few ways to fix the
thisbinding issue. One is usingbindto explicitly bindthis.bindreturns a new function withthispermanently bound. Another is using arrow functions, since they don’t have their ownthisand inherit it from the outer scope. You can also usecallorapplyto explicitly setthiswhen invoking the function.callandapplysetthisat invocation time.
Fixing
thisissues is essentially about ensuring the function is called with the correct context.
中文: 箭头函数的 this 和普通函数的 this 有什么区别?
English:
What is the difference between this in arrow functions and regular functions?
中文高分表达
箭头函数和普通函数最大的区别在于 this 的绑定方式不同。 普通函数的 this 是在调用时决定的,取决于调用方式; 而箭头函数没有自己的 this,它的 this 是在定义时就确定的,会继承外层作用域的 this。
English 口语版
The main difference is how
thisis determined. For regular functions,thisis decided at call time depending on how the function is invoked. For arrow functions,thisis lexically bound, meaning it is determined at definition time and inherited from the outer scope.
中文: 什么是事件循环(Event Loop)?你可以用自己的话解释一下。
English: What is the Event Loop in JavaScript? Explain it in your own words.
"The Event Loop is a coordination mechanism provided by the JS runtime (like the browser or Node.js). Its job is to handle asynchronous tasks by constantly monitoring the Call Stack and the Task Queues."
"事件循环是 JS 运行时(浏览器或 Node.js)提供的一种调度机制。它的职责是通过不断监控调用栈和任务队列,来协调异步任务的执行。"
Promises).
(栈空了,立即清空所有微任务,比如 Promise.then。)setTimeout) and pushes it onto the stack.
(之后,从宏任务队列里只取一个任务去执行,然后重复上述步骤。)"It allows JavaScript to be non-blocking despite being single-threaded. This is how we can handle I/O and network requests without freezing the UI."
"它让 JS 作为一个单线程语言也能实现非阻塞。这就是为什么我们处理网络请求时,界面不会卡死。"
事件循环不是 JS 语言本身的语法,而是 JS 运行时(浏览器或 Node.js)提供的一种调度逻辑。 它利用“任务队列”和“调用栈”的配合,解决了单线程无法处理高并发异步任务的问题。它的核心职责是:不断地监控调用栈(Call Stack),并在调用栈为空时,协调异步任务进入栈中执行。
核心模型——“主线程”与“任务队列”
要理解这个顺序,你需要脑补浏览器的三个核心角色:
Promise.then、MutationObserver。它是“VIP 通道”。setTimeout、setInterval、DOM 事件、I/O。它是“普通通道”。执行规则(这非常重要):
中文: 什么是宏任务(macro task)和微任务(micro task)?它们有什么区别?
English: What are macro tasks and micro tasks? What is the difference between them?
中文口语版
宏任务和微任务都是异步任务,只是优先级不同。 微任务的优先级比宏任务更高,会在当前宏任务执行完之后立即执行。
常见的宏任务有:setTimeout、setInterval、还有整体的 script 代码。
常见的微任务有:Promise 、 MutationObserver。
每执行完一个宏任务,都会立即清空所有微任务队列,然后才会执行下一个宏任务。
宏任务是一个个执行的,而每个宏任务之间都会先执行完所有微任务。
English 口语版
Macro-tasks and micro-tasks are both asynchronous, but the Event Loop treats them with different priorities to ensure UI responsiveness.
For Macro-tasks (宏任务), they represent independent pieces of work. Common examples include
setTimeout,setInterval, I/O operations, and the initial script execution.For Micro-tasks (微任务), they are urgent, smaller tasks that must be executed as soon as possible after the current execution context. The most common examples are Promises (
.then/.catch/await) andMutationObserver.
重要理念澄清:
宏任务和微任务都是异步的。我不能理解的就是为什么整体的script脚本也是宏任务,那岂不是整体的script脚本也是异步的?
Why is the initial script considered a macro-task? Because the Event Loop needs an entry point. The browser treats the loading of the script as the very first task in the queue. (为什么初始脚本被视为宏任务?因为事件循环需要一个入口点。浏览器将脚本的加载和执行视为队列中的第一个任务。)
Technically, synchronous code is the content of the currently running macro-task. Once that content is finished, the engine checks for micro-tasks before picking up the next macro-task like a
setTimeout. (从技术上讲,同步代码是当前运行中的宏任务的内容。一旦内容执行完毕,引擎会在抓取下一个宏任务(如setTimeout)之前检查微任务。)总结
同步代码是宏任务的内部逻辑。 宏任务是 Event Loop 的调度单位。
下面这段可能刚开始看很难理解,但是仔细研究一下,是重要的理念澄清:
问:是不是只有同步代码,没有同步任务一说呢? 是不是只有异步任务,没有异步代码一说呢?严格来说的话
答:
你说得非常精准,这其实触及了计算机科学中“执行” (Execution) 和 “调度” (Scheduling) 的本质区别。
如果我们要用最严格的术语来区分,可以这样理解:
1. 只有“同步代码”,没有“同步任务”?
准确。 在 Event Loop 的语境下,“任务 (Task)”这个词本身就隐含了 “排队” 和 “调度” 的意思。
- 代码 (Code): 指的是具体的指令。同步代码就是立即执行的指令,它们直接进入执行栈 (Call Stack)。
- 任务 (Task): 指的是被放进任务队列 (Queue) 等待被执行的一个“包裹”。
- 为什么没有“同步任务”? 因为如果是同步的,它就直接在执行栈里跑了,根本不需要去队列里“排队等待调度”。所以,当我们说“第一个宏任务”时,其实是指“这个任务包裹里装的是同步执行的脚本”。
2. 只有“异步任务”,没有“异步代码”?
准确。 其实并没有代码是“天生异步”的,代码永远是同步执行的,只是执行的时机被“异步化”了。
- 异步 (Asynchronous): 是一种调度手段。它告诉引擎:“不要现在跑这段代码,把它包成一个任务,放到队列里,等我手头的活(同步代码)干完了再回头跑它。”
- 任务 (Task/Job): 异步的载体。无论是一个
setTimeout的回调,还是一个.then()的回调,当它们真正被引擎执行的那一刻,它们内部的代码依然是按顺序同步执行的。总结:最严格的说法
- 同步 (Synchronous):描述的是执行方式——“立刻、按顺序、阻塞式”。
- 异步 (Asynchronous):描述的是调度方式——“稍后、排队、非阻塞”。
- 任务 (Task):描述的是 Event Loop 的工作单元。
面试时的专业表述建议:
"Strictly speaking, all code is executed synchronously within the call stack. 'Asynchronous' is simply a mechanism to schedule a block of code (a Task or a Job) to be executed at a later point in time by the Event Loop."
(严格来说,所有代码都是在执行栈中同步执行的。“异步”仅仅是一种机制,用于调度一段代码(任务或作业),让事件循环在未来的某个时间点执行它。)
举个例子强化理解:
当你写:
xxxxxxxxxxconsole.log('A'); // 同步代码setTimeout(() => {console.log('B'); // 异步任务里的同步代码}, 0);
- 引擎遇到
console.log('A'),这是同步代码,直接进栈,执行,出栈。- 引擎遇到
setTimeout,它是一个同步的 API 调用,但它的作用是注册一个异步任务。- 等到 0ms 后,回调函数
() => { console.log('B') }作为一个 “任务” 进队。- Event Loop 发现栈空了,把这个任务抓出来。此时,
console.log('B')依然是同步地进栈执行。你对这种“异步只是延迟了的同步执行”的视角认可吗?这种清醒的认识会让你在看代码时,不再被“异步”这个词搞晕。
接下来,我们要不要拆解一下
async/await?它最容易让人混淆“同步”和“异步”的界限。
中文: 下面代码输出什么?
console.log(1);setTimeout(() => { console.log(2);}, 0);Promise.resolve().then(() => { console.log(3);});console.log(4);English: What is the output of this code?
我的答案:1,4,3,2。完全正确。
中文口语版
这段代码会先执行同步代码,所以先输出 1 和 4。 然后 Promise.then 是微任务,会在当前宏任务结束后立即执行,所以输出 3。 setTimeout 是宏任务,会在下一轮事件循环中执行,所以最后输出 2。
English 口语版
First, the synchronous code runs, so it prints 1 and 4. Then, the Promise.then is a micro task, which runs right after the current macro task, so it prints 3. Finally, setTimeout is a macro task, so it runs in the next event loop, printing 2.
中文: 下面代码输出什么?
xxxxxxxxxxconsole.log(1);setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); });}, 0);Promise.resolve().then(() => { console.log(4);});console.log(5);English: What is the output of this code?
我的答案:1,5,4,2,3。完全正确。
中文版(推荐直接说)
先执行同步代码,所以先输出 1 和 5。 然后 Promise.then 是微任务,会在当前宏任务结束后执行,所以输出 4。 接着进入下一轮宏任务,执行 setTimeout,先输出 2。 在这个 setTimeout 里面又产生了一个 Promise.then,这是微任务,所以会在当前宏任务结束后立即执行,输出 3。
English 版
First, the synchronous code runs, so it prints 1 and 5. Then, the Promise.then is a micro task, so it runs after the current macro task and prints 4. Next, the setTimeout callback runs as a macro task and prints 2. Inside that callback, another Promise.then is scheduled, which is a micro task, so it runs immediately after and prints 3.
中文: 什么是原型(prototype)?什么是原型链(prototype chain)?
English: What is a prototype, and what is the prototype chain in JavaScript?
中文(推荐直接说)
原型可以理解为 JavaScript 对象之间共享属性和方法的一种机制。 每个对象都有一个隐式的原型,也就是 prototype,通过这个原型可以访问它没有定义的属性或方法。 而原型链就是由一个对象通过它的原型,一层一层往上查找,直到找到 null 为止的这个结构。
JavaScript 查找属性的过程,本质就是原型链的查找过程。
English(口语版)
A prototype is a mechanism in JavaScript that allows objects to share properties and methods. Every object has an internal prototype, which it can use to access properties or methods that are not defined on itself. The prototype chain is the structure formed when an object keeps looking up through its prototype until it reaches null.
Property lookup in JavaScript is essentially a traversal of the prototype chain.
中文: 构造函数、实例对象、prototype 之间是什么关系?
English: What is the relationship between constructor function, instance, and prototype?
中文(推荐直接说)
在 JavaScript 中,构造函数、实例对象和 prototype 之间是一个关联结构。 构造函数有一个 prototype 属性,这个 prototype 上可以挂共享的方法。 当我们用 new 调用构造函数时,会创建一个实例对象,这个实例对象内部会有一个隐式属性指向构造函数的 prototype。 所以实例对象可以通过这个原型访问到构造函数 prototype 上的方法。
English(口语版)
In JavaScript, constructor functions, instances, and prototypes are connected. A constructor function has a prototype property where shared methods are defined. When we use
new, an instance is created, and its internal prototype points to the constructor’s prototype. This allows the instance to access methods defined on the prototype.
中文:
__proto__ 和 prototype 有什么区别?
English:
What is the difference between __proto__ and prototype?
中文(推荐直接说)
prototype 是函数才有的属性,它用来定义构造函数创建出来的实例的共享方法。 而
__proto__是对象上的一个隐式属性,它指向创建这个对象的构造函数的 prototype。 简单来说,prototype 是“模板”,__proto__是“链接”,用于构成原型链。 一般开发中不推荐直接使用__proto__,更推荐使用 Object.getPrototypeOf。
English(口语版)
prototypeis a property of functions, it's used to define shared methods for instances created by the constructor.__proto__is an internal reference on objects that points to the constructor’s prototype. Simply put,prototypeis the “template”, and__proto__is the “link” that forms the prototype chain. In practice,__proto__is not recommended, andObject.getPrototypeOfis preferred.
__proto__,英文中怎么发音?
- 推荐读法:Dunder Proto (最专业)
在 Python 和 JavaScript 社区,双下划线(Double Underscore)通常被简称为 "Dunder"。
- 读音:
/ˈdʌndər ˈproʊtoʊ/- 写法: Dunder proto
- 原因: 这样读非常简洁,且对方一听就知道你在说带有双下划线的内部属性。
- 字面读法:Double Underscore Proto (最清晰)
如果你担心对方听不懂 "Dunder",可以直接读出下划线。
- 读法: Double underscore proto
- 原因: 毫无歧义,适合在正式讲解或对方不太熟悉术语时使用。
- 简化读法:Proto (最随意)
在上下文已经很明确是在聊原型链时,很多人会直接省略下划线。
- 读法: Proto
- 注意: 虽然省事,但在面试中最好先用一次全称(如 Dunder proto),之后再简化。
深入理解:
1️⃣ prototype(函数有)
👉 用来“提供共享方法”
xxxxxxxxxxfunction Person() {}Person.prototype.sayHi = function () {}2️⃣ proto(对象有)
👉 用来“指向原型”
xxxxxxxxxxconst p = new Person();p.__proto__ === Person.prototype;
中文: 如果一个对象找不到某个属性,会发生什么?原型链是怎么工作的?
English: What happens when an object cannot find a property? How does the prototype chain work?
中文(推荐直接说)
当一个对象访问某个属性时,如果它本身没有这个属性,JavaScript 就会沿着它的原型链向上查找。 也就是先找自己的属性,如果没有,就去它的 proto 指向的原型对象上找,再没有就继续往上找,直到找到 Object.prototype。 如果一直找不到,最终返回 undefined。
English(口语版)
When an object tries to access a property, JavaScript first looks for it on the object itself. If it’s not found, it goes up the prototype chain via
__proto__, then checks the prototype object, and keeps going up. This continues until it reachesObject.prototype. If the property is still not found, it returnsundefined.
深入理解:
你可以这样理解(面试可以不说这么细,但要懂):
查找顺序:
xxxxxxxxxx对象本身 → prototype → prototype 的 prototype → ... → Object.prototype → null
举个例子:
xxxxxxxxxxfunction Person() {}Person.prototype.sayHi = function () {};const p = new Person();p.sayHi();查找过程:
中文: 什么是作用域(Scope)?JavaScript 中有哪些作用域?
English: What is scope in JavaScript? What types of scope are there?
中文(推荐直接说)
作用域可以理解为变量和函数的可访问范围,也就是在代码中哪些地方可以访问某个变量。 JavaScript 中主要有三种作用域:全局作用域、函数作用域,以及 ES6 之后引入的块级作用域。
作用域本质是 JavaScript 管理变量访问权限的一种机制。
English(口语版)
Scope refers to the area in code where variables and functions are accessible. In JavaScript, there are mainly three types of scope: global scope, function scope, and block scope introduced in ES6.
Scope is essentially a mechanism for managing variable accessibility in JavaScript.
三种作用域(面试必须会说)
1️⃣ 全局作用域
var a = 1;👉 整个文件都能访问
2️⃣ 函数作用域
function test() { var b = 2;}👉 只在函数内部有效
3️⃣ 块级作用域(ES6)
{ let c = 3;}👉 只在 {} 内有效
中文: 什么是作用域链(Scope Chain)?它是如何工作的?
English: What is the scope chain, and how does it work?
中文(推荐直接说)
作用域链可以理解为:当访问一个变量时,如果在当前作用域找不到,就会沿着它的外层作用域一层一层往上查找,直到全局作用域,如果还找不到就会报错。 这种从内到外逐级查找的结构,就叫作用域链。
作用域链的本质是“变量查找机制”,从当前作用域逐级向上查找。
English(口语版)
The scope chain is the mechanism where JavaScript looks up variables. If a variable is not found in the current scope, it will search in the outer scope step by step until it reaches the global scope. If it is still not found, an error is thrown. This chain of nested scopes is called the scope chain.
The scope chain is essentially a variable lookup mechanism from inner to outer scopes.
深入理解:
查找过程:
xxxxxxxxxx当前作用域 → 外层作用域 → 外层的外层 → 全局作用域 → undefined / ReferenceError
例子:
let a = 1;function outer() { let b = 2; function inner() { let c = 3; console.log(a, b, c); } inner();}查找过程:

常见误区:
❌ 错误理解:
作用域链是“函数之间的关系”
✔ 正确理解:
作用域链是“变量查找路径”
中文: 什么是变量提升(Hoisting)?
English: What is hoisting in JavaScript?
中文
变量提升是指在 JavaScript 执行代码之前,会先把变量和函数的声明提升到当前作用域的顶部。 对于 var 来说,变量会被提升,但只提升声明,不提升赋值,所以在赋值之前访问会得到 undefined。 而函数声明会整体提升。
变量提升发生在 JavaScript 的编译阶段,而不是执行阶段。
English(口语版)
Hoisting means that during the creation phase, JavaScript moves variable and function declarations to the top of their scope before execution. For
var, only the declaration is hoisted, not the assignment, so accessing it before initialization returnsundefined. Function declarations are fully hoisted.
Hoisting happens during the compilation phase, not the execution phase.
深入理解:
1️⃣ var 的提升
xxxxxxxxxxconsole.log(a);var a = 10;等价于:
xxxxxxxxxxvar a;console.log(a); // undefineda = 10;2️⃣ 函数提升(整体提升)
xxxxxxxxxxsayHi();function sayHi() { console.log("hi");}✔ 可以正常执行
中文: let 和 const 有变量提升吗?为什么会出现暂时性死区(TDZ)?
English: Do let and const have hoisting? What is the Temporal Dead Zone (TDZ)?
中文(推荐直接说)
let 和 const 也是存在变量提升的,但是它们的提升不会让变量在声明之前可以被访问。 在代码执行到声明之前,这一段区域叫暂时性死区(TDZ),在 TDZ 里面访问变量会直接报错。 所以本质上是“提升了,但不可用”。
English(口语版)
letandconstare also hoisted, but they are not initialized before execution reaches their declaration. The area before the declaration is called the Temporal Dead Zone (TDZ), and accessing the variable there will cause a ReferenceError. So they are hoisted, but not accessible before initialization.